package com.techsoft.plugins;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.dom4j.Document;
import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.techsoft.Plugin;

public class PluginManager {
	private static final Logger Log = LoggerFactory
			.getLogger(PluginManager.class);
	private File pluginDirectory;
	private PluginMonitor pluginMonitor;
	private ScheduledExecutorService executor;

	private Map<String, Plugin> plugins;
	private Map<String, PluginClassLoader> pluginLoaders;
	private Map<String, String> currentJars;
	private Map<String, String> lastSnapshotJars;
	private boolean isDebug;

	public boolean getIsDebug() {
		return isDebug;
	}

	public void setIsDebug(boolean isDebug) {
		this.isDebug = isDebug;
	}

	public PluginManager(File pluginDir) {
		this.pluginDirectory = pluginDir;
		plugins = new ConcurrentHashMap<String, Plugin>();
		pluginLoaders = new HashMap<String, PluginClassLoader>();
		currentJars = new HashMap<String, String>();
		lastSnapshotJars = new HashMap<String, String>();

		File[] files = pluginDir.listFiles(new FilenameFilter() {

			@Override
			public boolean accept(File dir, String name) {
				File file = new File(dir, name);
				return file.isDirectory();
			}
		});

		for (File file : files) {
			try {
				this.deletefile(file.getAbsolutePath());
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			}
		}

		pluginMonitor = new PluginMonitor();
	}

	public void start() {
		executor = new ScheduledThreadPoolExecutor(1);
		executor.scheduleWithFixedDelay(pluginMonitor, 0, 20, TimeUnit.SECONDS);
	}

	public void shutdown() {
		if (executor != null) {
			executor.shutdown();
		}

		for (String pluginName : plugins.keySet()) {
			this.unLoadPlugin(pluginName);
		}
		plugins.clear();
		pluginLoaders.clear();
		currentJars.clear();
		lastSnapshotJars.clear();
		pluginMonitor = null;
	}

	public String getRealPluginName(File jarFile) {
		String pluginName = jarFile.getName();
		pluginName = pluginName.substring(0, pluginName.lastIndexOf("."));
		pluginName = pluginName + String.valueOf(jarFile.lastModified());

		return pluginName;
	}

	// public boolean deleteDir(File dir) {
	// boolean result = false;
	// try {
	// if (dir.isDirectory()) {
	// String[] childDirs = dir.list();
	// List<String> children = new ArrayList<String>(
	// Arrays.asList(childDirs));
	// Collections.sort(children, new Comparator<String>() {
	// public int compare(String o1, String o2) {
	// if (o1.equals("lib")) {
	// return -1;
	// }
	// if (o2.equals("lib")) {
	// return 1;
	// } else {
	// return o1.compareTo(o2);
	// }
	// }
	// });
	// for (String file : children) {
	// boolean success = deleteDir(new File(dir, file));
	// if (!success) {
	// Log.info("PluginManager: Plugin remove: could not delete: "
	// + new File(dir, file));
	// return false;
	// }
	// }
	// }
	// result = !dir.exists() || dir.delete();
	// } catch (Exception e) {
	// Log.error(e.getMessage(), e);
	// }
	//
	// return result;
	// }

	public void unLoadPlugin(String pluginName) {
		if (pluginLoaders.get(pluginName) != null) {
			pluginLoaders.remove(pluginName);
		}

		Plugin plugin = plugins.get(pluginName);
		if (plugin != null) {
			try {
				plugin.destroyPlugin();
				plugins.remove(pluginName);
				PluginClassLoader pluginLoader = pluginLoaders
						.remove(pluginName);

				if (pluginLoader != null) {
					pluginLoader.unloadJarFiles();
				}
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			}

			plugin = null;
		}

		File dir = new File(pluginDirectory, pluginName);
		if (dir != null) {
			try {
				Thread.sleep(2000);
				System.gc();
				int count = 0;
				boolean notdeleted = !deletefile(dir.getAbsolutePath())
						&& count < 5;
				while (notdeleted) {
					Thread.sleep(8000);
					count++;
					System.gc();

					notdeleted = !deletefile(dir.getAbsolutePath())
							&& count < 5;
				}

				if (!notdeleted) {
					Log.info("plugin [" + pluginName + "] is removed!");
				}

			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			}
		}
	}

	public File findPluginXML(File contentDir) throws Exception {
		File[] f = contentDir.listFiles(new FilenameFilter() {

			@Override
			public boolean accept(File dir, String name) {
				return name.equalsIgnoreCase("plugin.xml");
			}
		});
		if (f.length > 0) {
			return f[0];
		} else {
			throw new Exception(contentDir + " plugin.xml do not exist!");
		}
	}

	public boolean loadPlugin(File jarFile) {
		boolean result = false;
		if (jarFile.isFile()) {
			String pluginName = getRealPluginName(jarFile);
			unLoadPlugin(pluginName);
			try {
				File contentDir = PluginJarUtils.unzip(jarFile,
						this.pluginDirectory.getAbsolutePath(), true);

				if (contentDir != null) {
					PluginClassLoader pluginClassLoader = new PluginClassLoader(
							Thread.currentThread().getContextClassLoader());

					pluginClassLoader.addDirectory(contentDir);
					File pluginxml = findPluginXML(contentDir);

					if (pluginxml != null) {
						Document document = XMLUtils
								.createUTF8Documnet(pluginxml);
						Element root = document.getRootElement();
						Element classNode = root.element("class");
						if (classNode != null) {
							String className = classNode.getTextTrim();
							ClassLoader oldLoader = Thread.currentThread()
									.getContextClassLoader();
							Thread.currentThread().setContextClassLoader(
									pluginClassLoader);
							try {
								Class<?> cls = pluginClassLoader
										.loadClass(className);
								Plugin plugin = (Plugin) cls.newInstance();
								plugin.initializePlugin(this, contentDir);

								plugins.put(pluginName, plugin);
								pluginLoaders
										.put(pluginName, pluginClassLoader);
								Log.info("plugin [" + pluginName
										+ "] is Loaded!");
								result = true;
							} finally {
								Thread.currentThread().setContextClassLoader(
										oldLoader);
							}
						}
					}
				}
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
				unLoadPlugin(pluginName);
				result = false;
			}
		}

		return result;
	}

	private class PluginMonitor implements Runnable {
		private boolean running = false;

		public PluginMonitor() {
			File[] jarFiles = PluginJarUtils.findJarsForArray(pluginDirectory);
			for (File jar : jarFiles) {
				String pluginName = getRealPluginName(jar);
				String pluginPath = jar.getAbsolutePath();
				currentJars.put(pluginName, pluginPath);
				lastSnapshotJars.put(pluginName, pluginPath);
				loadPlugin(jar);
			}
		}

		private void loadNewJar(Set<Entry<String, String>> last,
				Set<Entry<String, String>> curr) {
			try {
				Set<Entry<String, String>> currBak = new HashSet<Entry<String, String>>();
				currBak.addAll(curr);
				currBak.removeAll(last);

				Iterator<Entry<String, String>> iterator = currBak.iterator();
				while (iterator.hasNext()) {
					Entry<String, String> pluginEntry = iterator.next();
					loadPlugin(new File(pluginEntry.getValue()));
				}
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			}
		}

		private void unloadDeleteJar(Set<Entry<String, String>> last,
				Set<Entry<String, String>> curr) {
			try {
				Set<Entry<String, String>> currBak = new HashSet<Entry<String, String>>();
				currBak.addAll(last);
				currBak.removeAll(curr);
				Iterator<Entry<String, String>> iterator = currBak.iterator();
				while (iterator.hasNext()) {
					Entry<String, String> pluginEntry = iterator.next();
					unLoadPlugin(pluginEntry.getKey());
				}
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			}
		}

		private void analyze() {
			Set<Entry<String, String>> lasEntrys = lastSnapshotJars.entrySet();
			Set<Entry<String, String>> curEntrys = currentJars.entrySet();
			this.loadNewJar(lasEntrys, curEntrys);

			this.unloadDeleteJar(lasEntrys, curEntrys);

			lastSnapshotJars.clear();
			lastSnapshotJars.putAll(currentJars);
		}

		@Override
		public void run() {
			synchronized (this) {
				if (running) {
					return;
				}
				running = true;
			}
			try {
				currentJars.clear();
				currentJars.putAll(PluginJarUtils
						.findJarsForMap(pluginDirectory));

				analyze();
			} catch (Exception e) {
				Log.error(e.getMessage(), e);
			} finally {
				synchronized (this) {
					running = false;
				}
			}
		}
	}

	public boolean deletefile(String path) throws Exception {
		try {
			File file = new File(path);
			// 当且仅当此抽象路径名表示的文件存在且 是一个目录时，返回 true
			if (!file.isDirectory()) {
				file.delete();
			} else if (file.isDirectory()) {
				String[] filelist = file.list();
				for (int i = 0; i < filelist.length; i++) {
					File delfile = new File(path + File.separator + filelist[i]);
					if (!delfile.isDirectory()) {
						delfile.delete();
					} else if (delfile.isDirectory()) {
						deletefile(path + File.separator + filelist[i]);
					}
				}
				file.delete();
			}

		} catch (FileNotFoundException e) {
			Log.error("deletefile() Exception:" + e.getMessage());
		}
		return true;
	}
}
